Skip to content

feat(data): schema registry + default-off drift guard for tushare cache - #57

Merged
StackOverFlow11 merged 1 commit into
mainfrom
feat/data-schema-registry-guard
Jun 29, 2026
Merged

feat(data): schema registry + default-off drift guard for tushare cache#57
StackOverFlow11 merged 1 commit into
mainfrom
feat/data-schema-registry-guard

Conversation

@StackOverFlow11

Copy link
Copy Markdown
Owner

Data-layer: schema registry + default-off drift guard for the tushare cache

Next D-series step. Turns the cache's already-stored-but-unvalidated fields_hash into an explicit, default-off, opt-in schema drift guard.

Core insight (why the guard sits at the raw-input boundary)

Every _parse_* in data/cache/tushare_parsers.py hardcodes a fixed canonical column set and defensively fillnas missing columns. So the stored fields_hash is immune to tushare changing its source columns — the real corruption risk is: tushare removes/renames a source column a parser depends on → the parser silently fills NaN → the cache stores NaN with zero signal. The guard checks the raw frame before parsing.

What it does (all opt-in; data.cache.schema_guard.enabled=false by default)

Invariants

  • Default-off is byte-identical: _guarded_parse(None) is an exact parse(raw) passthrough; every pre-existing cache test passes unchanged; factor/alpha/portfolio/runtime/qfq/coverage semantics untouched; phase0 anchor ic_mean=0.9600, annual_return=0.8408.
  • Secret-clean: findings/logs/exceptions carry only endpoint + column names + 16-char SHA1; no token, no .config.json path, no data values.
  • Scope: daily TushareCache only; intraday schema guard deferred.

Acceptance (independently re-run)

  • pytest: 758 passed (+62 schema tests; full-suite delta +62 over the 696 baseline)
  • ruff: clean · all 19 config/*.yaml validate · run-phase0: 0.9600 / 0.8408
  • python -O: registry↔ALL_ENDPOINTS sync guard holds (assert→raise, survives optimization)
  • secret scan: 0 new token/.config.json references

Review (2 rounds, replacing the prior external reviewer)

Known follow-up (not in scope here)

Guard is wired into the pipeline/backtest cache path, not the live data-update warm path (qt/data_updater.py) — that is the natural place to enable drift detection next.

Add a declarative endpoint schema registry and a default-off, report-only
(or strict) drift guard that checks the RAW INPUT boundary of the tushare
read-through cache, BEFORE parsing.

Each `_parse_*` returns a fixed canonical column set and NaN-fills any
canonical column whose source column is absent, so the stored `fields_hash`
is immune to tushare changing its source columns. The real silent-corruption
risk is a missing/renamed SOURCE column -> parser fills NaN -> cache stores
NaN with zero signal. The highest-value check is therefore on the raw frame.

- data/cache/schema_registry.py: EndpointSchema + REGISTRY (one per endpoint,
  canonical columns/keys derived from tushare_specs; expected_canonical_hash
  uses the ledger's _fields_hash), SchemaDriftFinding, and a stateful
  SchemaGuard with 4 checks: #1 missing required source cols (HARD), #2 unknown
  extra source cols (WARNING, only outside required/optional/known_extra),
  #3 parsed canonical mismatch (HARD), #4 stored schema hash changed vs ledger
  history (HARD, deduped once/endpoint/run; a migration detector, prior=None is
  always clean). strict mode raises a secret-free RuntimeError on HARD.
  Per-endpoint required/optional/known_extra follows one documented rule:
  REQUIRED = tushare guarantees it (axis/raise, fields= request, or standard
  core); OPTIONAL = defensively filled and legitimately omittable (e.g.
  index_member_all level names, guarded in the direct feed path); known_extra =
  documented-but-ignored response columns so check #2 stays quiet on them.
  Registry/endpoint sync uses an explicit RuntimeError (not assert, -O safe).
- data/cache/tushare_cache.py: keyword-only schema_guard=None ctor arg; a single
  _guarded_parse wrapper at all 4 parse sites (None-guard => exact passthrough);
  schema_findings()/schema_summary() accessors.
- data/cache/coverage.py: read-only additive last_fields_hash(endpoint).
- qt/config.py: default-off SchemaGuardCfg on data.cache.schema_guard.
- qt/pipeline.py: build the guard in _build_cache only when enabled; one
  secret-free summary line in _log_run_cache_stats.
- tests/test_schema_registry.py: 62 network-free tests, incl. the forward lock
  (required cols sufficient, no NaN), the inverse lock (every optional col is
  droppable without raising), known_extra vs uncatalogued-column #2 behaviour,
  and the default-off byte-identical passthrough.

The guard never sees a token or a data value (only column + endpoint names).
Default-off => every existing config validates unchanged and the cache parse
sites are byte-identical; phase0 anchor stays ic_mean=0.9600/annual=0.8408.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant